﻿/*
   Copyright 2013 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.Text;
using AopAlliance.Intercept;
using Flower.Directory.Util;
using Flower.Logging;
using System.Threading.Tasks;
using System.Threading;
using Flower.Security;
using Flower.Services;
using Flower.Services.Data;
using Spring.Context.Support;
using Spring.Core.IO;
using System.Transactions;
using Flower.Actors;
using Flower.Services.Clients;

namespace Flower.Directory.Host.Default.Advices
{
    internal class ProcessorNotifier : IMethodInterceptor
    {
        private const int MaxQueueSize = 30;

        private class ProcessorInfo
        {
            public string Id;
            public string Name;
            public IProcessor NotificationInterface;
            public string PendingFolderId;
            public DateTime LastNotifiedAt = DateTime.MinValue;
        }

        private class CommandComparer : IComparer<Command>
        {
            public static readonly CommandComparer Instance = new CommandComparer();

            public int Compare(Command x, Command y)
            {
                return -DateTime.Compare(x.CreatedAt, y.CreatedAt);
            }
        }

        private abstract class Command : IMessage<ProcessorNotifier>
        {
            public readonly DateTime CreatedAt = DateTime.Now;
            public IMethodInvocation Invocation;

            public abstract void Process(ProcessorNotifier processor);
        }

        private class OnSaveCommand : Command
        {
            public override void Process(ProcessorNotifier processor)
            {
                processor.OnSave(this);
            }
        }

        private class OnMoveCommand : Command
        {
            public override void Process(ProcessorNotifier processor)
            {
                processor.OnMove(this);
            }
        }

        private class OnRemoveCommand : Command
        {
            public override void Process(ProcessorNotifier processor)
            {
                processor.OnRemove(this);
            }
        }

        internal const string ProcessorObjectName = "processor";

        private static readonly Log Log = LogManager.CreateLog(typeof(ProcessorNotifier).Name);

        private readonly Dictionary<string, Func<IMethodInvocation, Command>> _commandFactories;
        private readonly IActor<Command> _actor;
        private readonly Lazy<List<ProcessorInfo>> _processors;
        private readonly Lazy<string> _processorsFolderId;
        private readonly ReaderWriterLockSlim _processorsLock = new ReaderWriterLockSlim();

        private volatile IDirectory _directory;

        private int _notificationThreadsCount = 4;
        public int NotificationThreadsCount { set { _notificationThreadsCount = value; } }

        public ProcessorNotifier()
        {
            _commandFactories = new Dictionary<string, Func<IMethodInvocation, Command>>
            {
                { "Save", inv => new OnSaveCommand { Invocation = inv } },
                { "Move", inv => new OnMoveCommand { Invocation = inv } },
                { "Remove", inv => new OnRemoveCommand { Invocation = inv } }
            };

            _actor = new MultiActor<ProcessorNotifier, Command>
            (
                _notificationThreadsCount, 
                _ => 
                    new Actor<ProcessorNotifier, Command>
                    (
                        this, 
                        new PriorityQueue<Command>(CommandComparer.Instance),
                        MaxQueueSize
                    )
            );
            _actor.Error += (sender, args) => Log.Error("Unexpected error.", args.Exception);

            _processors = new Lazy<List<ProcessorInfo>>(LoadProcessors);
            _processorsFolderId = new Lazy<string>(GetProcessorsFolderId);
        }

        public object Invoke(IMethodInvocation invocation)
        {
            object result = invocation.Proceed();

            EnsureDirectory(invocation);

            try
            {
                Func<IMethodInvocation, Command> commandFactory;

                if (_commandFactories.TryGetValue(invocation.Method.Name, out commandFactory))
                {
                    var trn = Transaction.Current;

                    if (trn == null)
                    {
                        _actor.Send(commandFactory(invocation));
                    }
                    else
                    {
                        trn.TransactionCompleted += (sender, args) =>
                        {
                            if (args.Transaction.TransactionInformation.Status == TransactionStatus.Committed)
                            {
                                try
                                {
                                    _actor.Send(commandFactory(invocation));
                                }
                                catch (Exception ex)
                                {
                                    Log.Error("Failed to notify a processor.", ex);
                                }
                            }
                        };
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Warning("Error occured in intent to start processor notification.", ex);
            }

            return result;
        }

        private void EnsureDirectory(IMethodInvocation invocation)
        {
            if (_directory == null)
            {
                lock (this)
                {
                    if (_directory == null)
                    {
                        var directory = invocation.Target as IDirectory;

                        if (directory != null)
                        {
                            _directory = directory;
                        }
                        else
                        {
                            Log.Error(string.Format("The object wrapped by ProcessorNotifier is not IDirectory ({0})", invocation.Target));
                        }
                    }
                }
            }
        }

        private string GetProcessorsFolderId()
        {
            using (new AuthorizationOverride("Administrator"))
            {
                DirectoryEntry processorsFolder = _directory.Get("/Processors");

                if (processorsFolder == null)
                {
                    Log.Warning("No Processors folder found.");
                    return null;
                }

                return processorsFolder.Id;
            }
        }

        private List<ProcessorInfo> LoadProcessors()
        {
            using (new AuthorizationOverride("Administrator"))
            {
                IList<DirectoryEntry> processorEntries =
                    _directory
                        .GetChildren
                        (
                            "/Processors",
                            DirectoryEntryTypes.Processor,
                            SortProperty.Default,
                            SortOrder.Asc,
                            0,
                            long.MaxValue
                        );

                return
                    processorEntries
                        .Select
                        (
                            processorEntry => ProcessorEntryToProcessorInfo((Processor)processorEntry)
                        )
                        .ToList();
            }
        }

        private ProcessorInfo ProcessorEntryToProcessorInfo(Processor processorEntry)
        {
            return new ProcessorInfo
            {
                Id = processorEntry.Id,
                Name = processorEntry.Name,
                PendingFolderId = GetPendingFolderId(processorEntry.Name),
                NotificationInterface = GetProcessorService(processorEntry)
            };
        }

        private IProcessor GetProcessorService(Processor processor)
        {
            try
            {
                return DirectoryUtil.GetService<IProcessor>(processor, "processor");
            }
            catch(Exception ex)
            {
                Log.Warning
                (
                    string.Format
                    (
                        "Failed to obtain an instance of the notification interface of the processor {0}.", 
                        processor.Name
                    ), 
                    ex
                );
                return null;
            }
        }

        private string GetPendingFolderId(string processorName)
        {
            using (new AuthorizationOverride("Administrator"))
            {
                DirectoryEntryInfo pendingFolder = _directory.GetInfo(string.Concat("/Processors/", processorName, "/Pending"));

                if (pendingFolder != null)
                {
                    return pendingFolder.Id;
                }
                else
                {
                    Log.Warning(string.Format("No Pending folder found for processor {0}.", processorName));
                    return null;
                }
            }
        }

        private ProcessorInfo GetProcessor(IEnumerable<ProcessorInfo> processors, Func<ProcessorInfo, bool> criteria)
        {
            _processorsLock.EnterReadLock();

            try
            {
                return processors.FirstOrDefault(criteria);
            }
            finally
            {
                _processorsLock.ExitReadLock();
            }
        }

        private ProcessorInfo LoadProcessorByName(List<ProcessorInfo> processors, string name)
        {
            _processorsLock.EnterWriteLock();

            try
            {
                ProcessorInfo proc = processors.FirstOrDefault(p => p.Name == name);

                if (proc != null)
                {
                    return proc;
                }

                using (new AuthorizationOverride("Administrator"))
                {
                    var processorEntry =
                        _directory.Get(string.Concat("/Processors/", name)) as Processor;

                    if (processorEntry != null)
                    {
                        proc = ProcessorEntryToProcessorInfo(processorEntry);
                        processors.Add(proc);
                    }
                }

                return proc;
            }
            finally
            {
                _processorsLock.ExitWriteLock();
            }
        }

        private void RemoveProcessor(List<ProcessorInfo> processors, Func<ProcessorInfo, bool> criteria)
        {
            int procToRemove = -1;

            for (int i = 0; i < processors.Count; i++)
            {
                if (criteria(processors[i]))
                {
                    procToRemove = i;
                    break;
                }
            }

            if (procToRemove >= 0)
            {
                _processorsLock.EnterWriteLock();

                try
                {
                    IProcessor notificationInterface = processors[procToRemove].NotificationInterface;
                    processors.RemoveAt(procToRemove);

                    if (notificationInterface != null)
                    {
                        var procDisposable = notificationInterface as IDisposable;

                        if (procDisposable != null)
                        {
                            procDisposable.Dispose();
                        }
                    }
                }
                finally
                {
                    _processorsLock.ExitWriteLock();
                }
            }
        }

        private void Notify(string folder, DateTime commandCreatedAt)
        {
            List<ProcessorInfo> processors = _processors.Value;
            ProcessorInfo proc = null;

            if (DirectoryUtil.IsId(folder))
            {
                proc = GetProcessor(processors, p => p.PendingFolderId == folder);
            }
            else
            {
                string[] pathParts = DirectoryUtil.SplitPath(folder);

                if (pathParts.Length == 3 && pathParts[0] == "Processors")
                {
                    //Get or load processor info always; notify only if Pending folder has been changed.
                    var procInfo =
                        GetProcessor(processors, p => p.Name == pathParts[1]) ??
                        LoadProcessorByName(processors, pathParts[1]);

                    if (pathParts[2] == "Pending")
                    {
                        proc = procInfo;
                    }
                }
            }

            if (proc != null && proc.NotificationInterface != null)
            {
                try
                {
                    lock (proc)
                    {
                        if (proc.LastNotifiedAt < commandCreatedAt)
                        {
                            proc.NotificationInterface.CheckPendingProcesses();
                            proc.LastNotifiedAt = commandCreatedAt;
                        }
                    }
                }
                catch (WcfClientException ex)
                {
                    if (ex.InnerException is CommunicationException)
                    {
                        Log.Warning(string.Format("Failed to notify processor {0}: {1}.", proc.Name, ex.Message));
                    }
                    else
                    {
                        Log.Error(string.Format("Failed to notify processor {0}.", proc.Name), ex);
                    }
                }
                catch (Exception ex)
                {
                    Log.Error(string.Format("Failed to notify processor {0}.", proc.Name), ex);
                }
            }
        }

        private void OnSave(Command command)
        {
            var parent = command.Invocation.Arguments[0] as string;

            if (parent == null)
            {
                return;
            }

            Notify(parent, command.CreatedAt);

            Processor newProcessor = null;

            if (DirectoryUtil.IsId(parent))
            {
                if (parent == _processorsFolderId.Value)
                {
                    newProcessor = command.Invocation.Arguments[1] as Processor;
                }
            }
            else
            {
                if (parent == "/Processors")
                {
                    newProcessor = command.Invocation.Arguments[1] as Processor;
                }
            }

            if (newProcessor != null)
            {
                List<ProcessorInfo> processors = _processors.Value;

                _processorsLock.EnterWriteLock();

                try
                {
                    if (processors.All(p => p.Name != newProcessor.Name))
                    {
                        processors.Add(ProcessorEntryToProcessorInfo(newProcessor));
                    }
                }
                finally
                {
                    _processorsLock.ExitWriteLock();
                }
            }
        }

        private void OnMove(Command command)
        {
            var where = (string)command.Invocation.Arguments[1];

            Notify(where, command.CreatedAt);
        }

        private void OnRemove(Command command)
        {
            List<ProcessorInfo> processors = _processors.Value;
            var what = (string)command.Invocation.Arguments[0];
            
            _processorsLock.EnterUpgradeableReadLock();

            try
            {
                if (DirectoryUtil.IsId(what))
                {
                    RemoveProcessor(processors, p => p.Id == what);
                }
                else
                {
                    string[] pathParts = DirectoryUtil.SplitPath(what);

                    if (pathParts.Length == 2 && pathParts[0] == "Processors")
                    {
                        RemoveProcessor(processors, p => p.Name == pathParts[1]);
                    }
                }
            }
            finally
            {
                _processorsLock.ExitUpgradeableReadLock();
            }
        }
    }
}
